package il.technion.cs236369.webserver;

import javax.net.ServerSocketFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList;
import java.util.Properties;
import java.util.Queue;

import org.apache.http.params.BasicHttpParams;
import org.xml.sax.SAXException;

import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;

public class WebServer extends AbstractWebServer
{
	ServerSocket serverSocket;				// one socket for accepting incoming connections
	Queue<Socket> sockets = new LinkedList<Socket>();
	Queue<ExtendedHttpRequest> requests = new LinkedList<ExtendedHttpRequest>();
	LinkedList<SocketReader> socketReaders = new LinkedList<SocketReader>();
	LinkedList<RequestHandler> handlers = new LinkedList<RequestHandler>();
	
	String m_baseDir = null;
	String m_welcomeFile = null;
	int m_port = -1;
	int m_numOfSocketReaders = -1;
	int m_numOfRequestHandlers = -1;
	int m_capacityOfSocketQueue = -1;
	int m_capacityOfRequestQueue = -1;
	
	static Mappings map = null;
	
	// save given parameters to members
	@Inject
 	public WebServer(
			ServerSocketFactory srvSockFactory, 
			@Named("httpserver.net.port") int port,
			@Named("httpserver.app.baseDir") String baseDir,
			@Named("httpserver.app.welcomeFile") String welcomeFile,
			@Named("httpserver.threads.numSocketReaders") int numSocketReaders,
			@Named("httpserver.threads.numRequestHandlers") int numRequestHandlers,
			@Named("httpserver.queues.sizeSocketQueue") int sizeSocketQueue,
			@Named("httpserver.queues.sizeRequestQueue") int sizeRequestQueue)
			throws ClassNotFoundException
	{
		super(srvSockFactory, port, baseDir, welcomeFile, 
				numSocketReaders, numRequestHandlers,
				sizeSocketQueue, sizeRequestQueue);

		//parses the first config file - config
		parseConfig();
		
		//parses the second config file - config.xml
		map = new Mappings();
		try 
		{
			map.parseServlets();
		}
		catch (XPathExpressionException | ParserConfigurationException |
				SAXException | IOException e) 
		{
			System.err.println("Error: parsing servlets.");
			e.printStackTrace();
		} 
	}

	private boolean parseConfig()
	{
		File configFile = new File("config");
		Properties props = new Properties();
		FileInputStream in;

		try
		{
			in = new FileInputStream(configFile);
			props.load(in);
		} 
		catch (FileNotFoundException e)
		{
			System.err.println("Enable to read config file");
			e.printStackTrace();
			return false;
		}
		catch (IOException e)
		{
			System.err.println("Enable to load config file");
			e.printStackTrace();
			return false;
		}
		
		try
		{
			m_baseDir = props.getProperty("httpserver.app.baseDir").toLowerCase();
			m_port = Integer.valueOf(props.getProperty("httpserver.net.port")).intValue();
			m_welcomeFile = props.getProperty("httpserver.app.welcomeFile");
			m_numOfSocketReaders = Integer.valueOf(props.getProperty("httpserver.threads.numSocketReaders")).intValue();
			m_numOfRequestHandlers = Integer.valueOf(props.getProperty("httpserver.threads.numRequestHandlers")).intValue();
			m_capacityOfSocketQueue = Integer.valueOf(props.getProperty("httpserver.queues.sizeSocketQueue")).intValue(); 
			m_capacityOfRequestQueue = Integer.valueOf(props.getProperty("httpserver.queues.sizeRequestQueue")).intValue();
		}
		catch(NumberFormatException e)
		{
			System.err.println("Illiegal format in config file, check for errors");
			e.getStackTrace();
		}
		return true;
	}

	/**
	 * Fills the handlers pools
	 * @throws ParserConfigurationException, SAXException, IOException, XPathExpressionException
	 */
	private void createThreadPools()
	{
		for (int i=0; i < m_numOfSocketReaders; i++)
		{
			SocketReader socketReader = new SocketReader(sockets, requests, 
														m_capacityOfRequestQueue, 
														new BasicHttpParams(),
														m_baseDir, 
														m_welcomeFile, i+1); 
			socketReader.start();
			socketReaders.add(socketReader);
			//System.out.println("socket " + (i+1) +" was created");
		}
		for (int i=0; i < m_numOfRequestHandlers; i++)
		{
			RequestHandler handler = new RequestHandler(requests, i+1); 
			handler.start();
			handlers.add(handler);
			//System.out.println("handler " + (i+1) +" was created");
		}
	}
	
	/**
	 * Create a new bounded server socket using ServerSocketFactory with the
	 * given port, and fills pools with threads
	 * 
	 * @throws IOException unable to bind the server socket
	 */
	@Override
	public void bind() throws IOException 
	{
		serverSocket =  srvSockFactory.createServerSocket();
		serverSocket.bind(new InetSocketAddress(m_port));
		createThreadPools();
	}

	/**
	 * fills in socket readers instead of dead ones 
	 */
	private void fillSocketReaders()
	{
		// refilling dead socket readers
		LinkedList<SocketReader> deadSockets = new LinkedList<SocketReader>();
		for (int i=0; i < m_numOfSocketReaders; i++)
		{
			SocketReader socketReader = socketReaders.get(i);
			if (!socketReader.isAlive())
			{
				deadSockets.add(socketReader);
				System.out.println("spotted dead socket");
			}
		}
		for (int i=0; i < deadSockets.size(); i++)
		{
			socketReaders.remove(deadSockets.get(i));
		}
		for (int i=0; i < deadSockets.size(); i++)
		{
			SocketReader socketReader = new SocketReader(sockets, requests, 
														m_capacityOfRequestQueue, 
														new BasicHttpParams(), 
														m_baseDir, 
														m_welcomeFile, i+1);
			socketReader.start();
			socketReaders.add(socketReader);
		}
	}

	/**
	 * fills in request handlers instead of dead ones 
	 */
	private void fillRequestHandlers()
	{
		// refilling dead request handlers
		LinkedList<RequestHandler> deadhandler = new LinkedList<RequestHandler>();
		for (int i=0; i < m_numOfRequestHandlers; i++)
		{
			RequestHandler handler = handlers.get(i);
			if (!handler.isAlive())
			{
				deadhandler.add(handler);
			//	System.out.println("spotted dead handler");
			}
		}
		for (int i=0; i < deadhandler.size(); i++)
		{
			handlers.remove(deadhandler.get(i));
		}
		for (int i=0; i < deadhandler.size(); i++)
		{
			RequestHandler handler = new RequestHandler(requests, i+1); 
			handler.start();
			handlers.add(handler);
			//System.out.println("handler created");
		}
	}

	/**
	 * Check whether there are any dead socket readers/request handlers and regenerates them
	 */
	private void fillThreadPools()
	{
		fillSocketReaders();
		 
		fillRequestHandlers();
	}

	/**
	 * listen to clients request, when such arrives it puts it into the socket queue and continue listening
	 * @throws IOException unable to bind the server socket
	 */
	private void listen() throws IOException
	{
		while (true)
		{
			Socket socket;
			socket = serverSocket.accept();
			boolean notFull = !(sockets.size() >= m_capacityOfSocketQueue);
			
			if (notFull)
			{
				synchronized (sockets)
				{
					sockets.add(socket);
					sockets.notify();
				}
				//System.out.println("socket created");
			}
			else
			{
				HttpResponser responser = new HttpResponser();
				responser.sendErrorResponse(socket, 503, "Service Unavailable",
											"Server is temporarily unavailable, please try again later");
				System.err.println("Queue Socket is full");
			}
			//check if thread pools are full, if not refills them
			fillThreadPools();
		}
	}	

	@Override
	public void start() 
	{
		while(true)
		{
			try
			{
				listen();
			}
			catch (IOException e) 
			{
				System.err.println("Can't bind to server socket");
				e.printStackTrace();
				System.exit(0);
			}
		}
	}

	public static void main(String[] args) throws Exception 
	{
		Properties p = new Properties();
		p.load(new FileInputStream("config"));
		Injector inj = Guice.createInjector(new WebServerModule(p));
		IWebServer server = inj.getInstance(WebServer.class);
		server.bind();
		server.start();
	}
	
}
